import logging
import urllib
from collections import defaultdict
from contextlib import suppress
from urllib.parse import quote

from csp.decorators import csp_exempt
from django import forms
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db.models import CharField, Q
from django.db.models.functions import Lower
from django.http import FileResponse, Http404, HttpRequest
from django.shortcuts import get_object_or_404, redirect
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from django_context_decorator import context
from formtools.wizard.forms import ManagementForm
from i18nfield.forms import I18nModelForm
from rules.contrib.views import PermissionRequiredMixin

from eventyay.common.forms import SearchForm
from eventyay.common.text.phrases import phrases

SessionStore = import_string(f'{settings.SESSION_ENGINE}.SessionStore')
logger = logging.getLogger(__name__)


class ActionFromUrl:
    write_permission_required = None
    create_permission_required = None

    @cached_property
    def object(self):
        return self.get_object()

    @cached_property
    def permission_object(self):
        if hasattr(self, 'get_permission_object'):
            return self.get_permission_object()
        return self.object

    def _check_permission(self, permission_name):
        return self.request.user.has_perm(permission_name, self.permission_object)

    @context
    @cached_property
    def action(self):
        if not any(_id in self.kwargs for _id in ('pk', 'code')):
            if self._check_permission(self.create_permission_required or self.write_permission_required):
                return 'create'
            return 'view'
        if self._check_permission(self.write_permission_required):
            return 'edit'
        return 'view'

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['read_only'] = self.action == 'view'
        event = getattr(self.request, 'event', None)
        if event and issubclass(self.form_class, I18nModelForm):
            kwargs['locales'] = event.locales
        return kwargs


class Sortable:
    """
    Handles queryset sorting. Will figure out if a field is a text field and sort by
    Lower() if so.

    In the main class, you'll have to call sort_queryset() in get_queryset.
    In the template, do this:

    <th>
        {% trans "Title" %}
        <a href="{% querystring sort='-title' %}"><i class="fa fa-caret-down"></i></a>
        <a href="{% querystring sort='title' %}"><i class="fa fa-caret-up"></i></a>
    </th>
    """

    sortable_fields = []
    secondary_sort = {}
    default_sort_field = None

    def _get_secondary_sort(self, key):
        secondary_sort_config = getattr(self, 'secondary_sort', None) or {}
        return list(secondary_sort_config.get(key, []) or [])

    def _sort_queryset(self, qs, fields):
        fields = [key for key in fields if key]
        # If the model does not have a Meta.ordering, we need to add a
        # final sort key to make sure the sorting is stable.
        if not qs.model._meta.ordering and 'pk' not in fields and '-pk' not in fields:
            fields += ['pk']
        if fields:
            qs = qs.order_by(*fields)
        return qs

    def sort_queryset(self, qs):
        sort_key = self.request.GET.get('sort') or ''
        if not sort_key or sort_key == 'default':
            sort_key = getattr(self, 'default_sort_field', None) or ''
        plain_key = sort_key[1:] if sort_key.startswith('-') else sort_key
        if plain_key not in self.sortable_fields:
            return self._sort_queryset(qs, self._get_secondary_sort(''))

        is_text = False
        if '__' not in plain_key:
            with suppress(FieldDoesNotExist):
                is_text = isinstance(qs.model._meta.get_field(plain_key), CharField)
        else:
            split_key = plain_key.split('__')
            if len(split_key) == 2:
                is_text = isinstance(
                    qs.model._meta.get_field(split_key[0]).related_model._meta.get_field(split_key[1]),
                    CharField,
                )

        if is_text:
            # TODO: this only sorts direct lookups case insensitively
            # A sorting field like 'speaker__name' will not be found
            qs = qs.annotate(key=Lower(plain_key))
            sort_key = '-key' if plain_key != sort_key else 'key'

        return self._sort_queryset(qs, [sort_key] + self._get_secondary_sort(plain_key))


class Filterable:
    filter_fields = []
    default_filters = []

    def get_default_filters(self):
        return self.default_filters

    def filter_queryset(self, qs):
        if self.filter_fields:
            qs = self._handle_filter(qs)
        if 'q' in self.request.GET:
            query = urllib.parse.unquote(self.request.GET['q'])
            qs = self.handle_search(qs, query, self.get_default_filters())
        if (filter_form := self.filter_form) and filter_form.is_valid() and hasattr(filter_form, 'filter_queryset'):
            qs = filter_form.filter_queryset(qs)
        return qs

    def _handle_filter(self, qs):
        for key in self.request.GET:  # Do NOT use items() to preserve multivalue fields
            # There is a special case here: we hack in OR lookups by allowing __ in values.
            lookups = defaultdict(list)
            values = self.request.GET.getlist(key)
            for value in values:
                value_parts = value.split('__', maxsplit=1)
                if len(value_parts) > 1 and value_parts[0] in self.filter_fields:
                    _key = value_parts[0]
                    _value = value_parts[1]
                else:
                    _key = key
                    _value = value_parts[0]
                if _key in self.filter_fields and _value:
                    if '__isnull' in _key:
                        # We don't append to the list here, because that's not meaningful
                        # in a boolean lookup
                        lookups[_key] = _value == 'on'
                    else:
                        _key = f'{_key}__in'
                        lookups[_key].append(_value)
            _filters = Q()
            for _key, value in lookups.items():
                _filters |= Q(**{_key: value})
            qs = qs.filter(_filters)
        return qs

    @staticmethod
    def handle_search(qs, query, filters):
        _filters = [Q(**{field: query}) for field in filters]
        if len(_filters) > 1:
            _filter = _filters[0]
            for additional_filter in _filters[1:]:
                _filter = _filter | additional_filter
            qs = qs.filter(_filter)
        elif _filters:
            qs = qs.filter(_filters[0])
        return qs

    @context
    @cached_property
    def search_form(self):
        return SearchForm(self.request.GET if 'q' in self.request.GET else None)

    @context
    @cached_property
    def filter_form(self):
        if hasattr(self, 'filter_form_class'):
            return self.filter_form_class(self.request.GET, event=self.request.event)
        if hasattr(self, 'get_filter_form'):
            return self.get_filter_form()
        if self.filter_fields:
            _form = forms.modelform_factory(self.model, fields=self.filter_fields)(self.request.GET)
            for field in _form.fields.values():
                field.required = False
                if hasattr(field, 'queryset'):
                    field.queryset = field.queryset.filter(event=self.request.event)
            return _form
        return None


class PermissionRequired(PermissionRequiredMixin):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not hasattr(self, 'get_permission_object'):
            for key in ('permission_object', 'object'):
                if getattr(self, key, None):
                    self.get_permission_object = lambda self: getattr(self, key)  # noqa

    def has_permission(self):
        """Check if user has permission, with admin mode support.

        If the user has an active staff session (admin mode), grant full access.
        Otherwise, perform normal permission checks.
        """
        request = getattr(self, 'request', None)

        # Check for admin mode / active staff session
        if request and hasattr(request, 'user') and hasattr(request.user, 'has_active_staff_session') and request.user.has_active_staff_session(request.session.session_key):
            logger.debug(
                'User %s has active staff session - granting admin access to %s',
                request.user,
                request.path
            )
            return True


        # Normal permission check
        result = None
        result = super().has_permission()
        logger.debug(
            'Result of permission check (for %s on "%s") from parent class: %s',
            self.get_permission_required(),
            self.get_permission_object(),
            result,
        )
        if not result:
            if request and hasattr(request, 'event'):
                key = f'eventyay_event_access_{request.event.pk}'
                if key in request.session:
                    sparent = SessionStore(request.session.get(key))
                    parentdata = sparent.load()
                    return 'event_access' in parentdata
        return result

    def get_login_url(self):
        """We do this to avoid leaking data about existing pages."""
        logger.debug('Refuse to return login URL')
        raise Http404()

    def handle_no_permission(self):
        request = getattr(self, 'request', None)
        if not request or not isinstance(request, HttpRequest):
            raise ImproperlyConfigured('PermissionRequiredMixin requires a request.')
        # Debug log to trace why 404 happened
        logger.debug('User %s has no permission to access %s', request.user, request.path)
        if (
            request
            and hasattr(request, 'event')
            and request.user.is_anonymous
            and 'cfp' in request.resolver_match.namespaces
        ):
            params = '&' + request.GET.urlencode() if request.GET else ''
            return redirect(request.event.urls.login + f'?next={quote(request.path)}' + params)
        raise Http404()


class EventPermissionRequired(PermissionRequired):
    def get_permission_object(self):
        return self.request.event


class SensibleBackWizardMixin:
    def post(self, *args, **kwargs):
        """Don't redirect if user presses the prev.

        step button, save data instead. The rest of this is copied from
        WizardView. We want to save data when hitting "back"!
        """
        wizard_goto_step = self.request.POST.get('wizard_goto_step')
        management_form = ManagementForm(self.request.POST, prefix=self.prefix)
        if not management_form.is_valid():
            raise forms.ValidationError(
                _('ManagementForm data is missing or has been tampered with.'),
                code='missing_management_form',
            )

        form_current_step = management_form.cleaned_data['current_step']
        if form_current_step != self.steps.current and self.storage.current_step is not None:
            # form refreshed, change current step
            self.storage.current_step = form_current_step

        # get the form for the current step
        form = self.get_form(data=self.request.POST, files=self.request.FILES)

        # and try to validate
        if form.is_valid():
            # if the form is valid, store the cleaned data and files.
            self.storage.set_step_data(self.steps.current, self.process_step(form))
            self.storage.set_step_files(self.steps.current, self.process_step_files(form))

            # check if the current step is the last step
            if wizard_goto_step and wizard_goto_step in self.get_form_list():
                return self.render_goto_step(wizard_goto_step)
            if self.steps.current == self.steps.last:
                # no more steps, render done view
                return self.render_done(form, **kwargs)
            # proceed to the next step
            return self.render_next_step(form)
        return self.render(form)


class SocialMediaCardMixin:
    def get_image(self):
        raise NotImplementedError

    @csp_exempt()
    def get(self, request, *args, **kwargs):
        try:
            image = self.get_image()
            if image:
                return FileResponse(image)
        except Exception:
            pass
        if self.request.event.logo:
            return FileResponse(self.request.event.logo)
        if self.request.event.header_image:
            return FileResponse(self.request.event.header_image)
        raise Http404()


class PaginationMixin:
    DEFAULT_PAGINATION = 50

    def get_paginate_by(self, queryset=None):
        skey = 'stored_page_size_' + self.request.resolver_match.url_name
        default = self.request.session.get(skey) or getattr(self, 'paginate_by', None) or self.DEFAULT_PAGINATION
        if self.request.GET.get('page_size'):
            try:
                max_page_size = getattr(self, 'max_page_size', 250)
                size = min(max_page_size, int(self.request.GET.get('page_size')))
                self.request.session[skey] = size
                return size
            except ValueError:
                return default
        return default

    def get_context_data(self, **kwargs):
        from eventyay.common.views.generic import CRUDView

        ctx = super().get_context_data(**kwargs)
        if isinstance(self, CRUDView) and not self.action == 'list':
            return ctx
        ctx['page_size'] = self.get_paginate_by(None)
        ctx['pagination_sizes'] = [50, 100, 250]
        return ctx


class ActionConfirmMixin:
    """
    Mixin providing all variables needed for the action_confirm.html template,
    which you can either include via common/includes/action_confirm.html, or
    use directly as common/action_confirm.html.

    Implement at least:
    - action_object_name

    Implement probably:
    - action_back_url

    If the view is not a delete view, also implement:
    - action_confirm_label
    - action_confirm_color
    - action_confirm_icon
    - action_title
    - action_text
    """

    template_name = 'common/action_confirm.html'
    action_object_name = None  # Shown between the title and the warning text
    action_text = phrases.base.delete_warning  # Use this for context or warnings
    action_title = phrases.base.delete_confirm_heading  # Shown as heading and as title
    action_confirm_color = 'danger'
    action_confirm_icon = 'trash'
    action_confirm_label = phrases.base.delete_button
    action_confirm_name = None
    action_confirm_value = None
    action_back_color = 'outline-info'
    action_back_icon = None
    action_back_label = phrases.base.back_button

    @property
    def additional_actions(self):
        # Actions can be links or buttons, and should be a list of dicts:
        # optional attributes: label, color, icon
        # links additionally have href
        # buttons additionall have name, value
        return []

    @property
    def action_back_url(self):
        url_param = self.request.GET.get('next') or self.request.GET.get('back')
        if url_param:
            return urllib.parse.unquote(url_param)
        # Fallback if we don't have a next parameter: go up one level
        return self.request.path.rsplit('/', 2)[0]

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['action_additional_actions'] = self.additional_actions
        ctx['action_back_color'] = self.action_back_color
        ctx['action_back_icon'] = self.action_back_icon
        ctx['action_back_label'] = self.action_back_label
        ctx['action_back_url'] = self.action_back_url
        ctx['action_confirm_color'] = self.action_confirm_color
        ctx['action_confirm_icon'] = self.action_confirm_icon
        ctx['action_confirm_label'] = self.action_confirm_label
        ctx['action_confirm_name'] = self.action_confirm_name
        ctx['action_confirm_value'] = self.action_confirm_value
        ctx['action_text'] = self.action_text
        ctx['action_title'] = self.action_title
        ctx['action_object_name'] = self.action_object_name
        return ctx


class OrderActionMixin:
    """Change an ordered model with a POST endpoint to a CRUDView list view."""

    extra_actions = {
        'list': {'post': 'order_handler'},
    }

    def order_handler(self, request, *args, **kwargs):
        order = request.POST.get('order')
        if order:
            order = order.split(',')
            queryset = self.get_queryset()
            for index, pk in enumerate(order):
                obj = get_object_or_404(queryset, pk=pk)
                obj.position = index
                obj.save(update_fields=['position'])
        return self.list(request, *args, **kwargs)
